/* SignalMLCodecSampleSource.java created 2007-09-24
 *
 */

package org.signalml.domain.signal.samplesource;

import java.util.Arrays;

import org.apache.log4j.Logger;
import org.signalml.codec.SignalMLCodecException;
import org.signalml.codec.SignalMLCodecReader;
import org.signalml.domain.signal.MultichannelSignalResampler;
import org.signalml.domain.signal.NaiveMultichannelSignalResampler;
import org.signalml.plugin.export.SignalMLException;
import org.signalml.plugin.export.change.listeners.PluginSignalChangeListener;
import org.signalml.util.Util;

/**
 * This class represents the source of samples from the signal described
 * in the SignalML markup language.
 *
 * @see SignalMLCodecReader
 * @author Michal Dobaczewski &copy; 2007-2008 CC Otwarte Systemy Komputerowe Sp. z o.o.
 */
public class SignalMLCodecSampleSource extends AbstractMultichannelSampleSource implements OriginalMultichannelSampleSource, ResamplableSampleSource {

	protected static final Logger logger = Logger.getLogger(SignalMLCodecSampleSource.class);

	/**
	 * the {@link SignalMLCodecReader reader} that gets all informations
	 * about the signal from file
	 */
	private SignalMLCodecReader reader = null;

	/**
	 * the number of samples per second
	 */
	private float samplingFrequency;

	/**
	 * the number of channels
	 */
	private int channelCount;

	/**
	 * the calibration of the signal
	 */
	private float calibration;

	/**
	 * the number of samples in the single channel
	 */
	private int sampleCount;

	/**
	 * if this source is capable of returning a the number of samples
	 * per second
	 */
	private boolean samplingFrequencyCapable = false;
	/**
	 * if this source is capable of returning the number of channels
	 */
	private boolean channelCountCapable = false;
	/**
	 * if this source is capable of returning a calibration
	 */
	private boolean calibrationCapable = false;

	/**
	 * true if frequency of sampling is equal for all channels, false
	 * otherwise
	 */
	private boolean uniformSampling = true;

	/**
	 * an array containing for each channel the number of samples per second
	 */
	private float[] channelSampling;

	/**
	 * an array of labels of channels
	 */
	private String[] labels;

	/**
	 * informs whether the sampling frequency has changed
	 */
	private boolean samplingFrequencyExternal = false;
	/**
	 * informs whether the channel count has changed
	 */
	private boolean channelCountExternal = false;
	/**
	 * informs whether the calibration has changed
	 */
	private boolean calibrationExternal = false;

	/**
	 * the {@link MultichannelSignalResampler resampler} of the signal
	 */
	private MultichannelSignalResampler resampler = null;

	/**
	 * Constructor. Creates the source of samples based on a given
	 * {@link SignalMLCodecReader reader}.
	 * @param reader the reader that gets all informations about signal
	 * from file
	 * @throws SignalMLException if codec doesn't support max offset, so
	 * is unusable
	 */
	public SignalMLCodecSampleSource(SignalMLCodecReader reader) throws SignalMLException {
		super();
		this.reader = reader;

		try {
			sampleCount = reader.get_max_offset() + 1;
		} catch (SignalMLCodecException ex) {
			logger.error("Codec doesn't support max offset - unusable");
			throw ex;
		}

		try {
			channelCountCapable = reader.is_number_of_channels();
		} catch (SignalMLCodecException e) {
			logger.warn("WARNING: codec doesn't support is_number_of_channels, left false");
			logger.debug("Caught exception was", e);
			channelCountCapable = false;
		}

		if (channelCountCapable) {
			try {
				channelCount = reader.get_number_of_channels();
			} catch (SignalMLCodecException e) {
				logger.warn("WARNING: codec doesn't support channel count, assumed 1");
				logger.debug("Caught exception was", e);
				channelCount = 1;
				channelCountCapable = false;
			}
		}

		try {
			samplingFrequencyCapable = reader.is_sampling_frequency();
		} catch (SignalMLCodecException e) {
			logger.warn("WARNING: codec doesn't support is_sampling_frequency, left false");
			logger.debug("Caught exception was", e);
		}

		if (samplingFrequencyCapable) {

			try {
				uniformSampling = reader.is_uniform_sampling_frequency();
			} catch (SignalMLCodecException e) {
				logger.warn("WARNING: codec doesn't support uniform sampling frequency info, left true");
				logger.debug("Caught exception was", e);
			}

			if (!uniformSampling) {

				logger.warn("WARNING: signal sampling is not uniform. Naive upsampling will be used for select channels");

				resampler = new NaiveMultichannelSignalResampler();
				collectChannelSampling();

			} else {

				try {
					samplingFrequency = reader.get_sampling_frequency();
				} catch (SignalMLCodecException e) {
					logger.warn("WARNING: codec doesn't support sampling frequency, left null");
					logger.debug("Caught exception was", e);
				}
			}
		}


		try {
			this.calibrationCapable = reader.is_calibration();
		} catch (SignalMLCodecException e) {
			logger.warn("WARNING: codec doesn't support is_callibration, assumed false");
			logger.debug("Caught exception was", e);
		}

		readLabels();

		logger.info("sampleCount = " + sampleCount);
		logger.info("samplingFrequency = " + samplingFrequency);
	}

	/**
	 * Reads the frequency of sampling (number of samples per second) for
	 * each channel from file.
	 */
	private void collectChannelSampling() {

		channelSampling = new float[channelCount];

		for (int i=0; i<channelCount; i++) {

			try {
				channelSampling[i] = reader.get_sampling_frequency(i);
				if (channelSampling[i] > samplingFrequency) {
					samplingFrequency = channelSampling[i];
				}
			} catch (SignalMLCodecException e) {
				logger.warn("WARNING: codec didn't return channel sampling for channel [" + i + "] - assumed default");
				logger.debug("Caught exception was", e);
				channelSampling[i] = 0;
			}

		}

		for (int i=0; i<channelCount; i++) {
			if (channelSampling[i] == 0) {
				channelSampling[i] = samplingFrequency;
			}
		}
	}

	/**
	 * Reads the labels of channels from file. If some labels are missing
	 * the default labels are used.
	 */
	private void readLabels() {

		labels = null;

		boolean hasNames = false;
		try {
			hasNames = reader.is_channel_names();
		} catch (SignalMLCodecException e) {
			logger.warn("WARNING: codec doesn't support is_channel_names, assumed false");
			logger.debug("Caught exception was", e);
		}
		if (hasNames) {
			try {
				labels = reader.get_channel_names();
			} catch (SignalMLCodecException e) {
				logger.warn("WARNING: codec doesn't support get_channel_names");
				logger.debug("Caught exception was", e);
			}
		}

		if (labels == null) {
			return;
		}

		int i;
		for (i=0; i<labels.length; i++) {

			labels[i] = Util.trimString(labels[i]);

			if (labels[i] == null || labels[i].length() == 0) {
				labels[i] = "L" + i;
			}

		}

		if (labels.length < channelCount) {
			logger.debug("some labels missing");
			String[] newLabels = new String[channelCount];
			for (i=0; i<labels.length; i++) {
				newLabels[i] = labels[i];
			}
			for (; i<channelCount; i++) {
				newLabels[i] = "L" + i;
			}
			labels = newLabels;
		}

	}

	/**
	 * Returns the {@link SignalMLCodecReader reader} that gets all
	 * informations about the signal from file
	 * @return the reader that gets all informations about the signal
	 * from file
	 */
	public SignalMLCodecReader getReader() {
		return reader;
	}

	@Override
	public float getSamplingFrequency() {
		return samplingFrequency;
	}

	@Override
	public int getChannelCount() {
		return channelCount;
	}

	/**
	 * Returns if this source is capable of returning a calibration
	 * @return true if this source is capable of returning a
	 * calibration, false otherwise
	 */
	@Override
	public boolean isCalibrationCapable() {
		return calibrationCapable;
	}

	/**
	 * Returns if this source is capable of returning a sampling
	 * frequency
	 * @return true if this source is capable of returning a
	 * sampling frequency, false otherwise
	 */
	@Override
	public boolean isSamplingFrequencyCapable() {
		return samplingFrequencyCapable;
	}

	/**
	 * Returns if this source is capable of returning a channel count
	 * @return true if this source is capable of returning a
	 * channel count, false otherwise
	 */
	@Override
	public boolean isChannelCountCapable() {
		return channelCountCapable;
	}

	@Override
	public float[] getCalibrationGain() {
		throw new UnsupportedOperationException("Calling float[] getCalibrationGain is not supported for SignalMLCodecSampleSource.");
	}

	@Override
	public String getLabel(int channel) {
		if (channel < 0 || channel >= channelCount) {
			throw new IndexOutOfBoundsException("Bad channel number [" + channel + "]");
		}
		if (labels == null) {
			labels = new String[channelCount];
			for (int i=0; i<channelCount; i++) {
				labels[i] = "L" + (i+1);
			}
		}
		return labels[channel];
	}

	@Override
	public int getDocumentChannelIndex(int channel) {
		return channel;
	}

	@Override
	public int getSampleCount(int channel) {
		return sampleCount;
	}

	/**
	 * Returns the given number of samples for a given channel starting
	 * from a given position in time.
	 * If sampling frequency for the channel is different from default
	 * the signal is {@link MultichannelSignalResampler resampled}.
	 * @param channel the number of channel
	 * @param target the array to which results will be written starting
	 * from position <code>arrayOffset</code>
	 * @param signalOffset the position (in time) in the signal starting
	 * from which samples will be returned
	 * @param count the number of samples to be returned
	 * @param arrayOffset the offset in <code>target</code> array starting
	 * from which samples will be written
	 * @throws IndexOutOfBoundsException if channel of given index doesn't
	 * exist
	 * or some of the requested samples are not in the signal
	 * or created result doesn't fit in the <code>target</code> array
	 */
	@Override
	public void getSamples(int channel, double[] target, int signalOffset, int count, int arrayOffset) {
		synchronized (this) {
			if (channel < 0 || channel >= channelCount) {
				throw new IndexOutOfBoundsException("Bad channel number [" + channel + "]");
			}
			if ((signalOffset < 0) || ((signalOffset + count) > sampleCount)) {
				throw new IndexOutOfBoundsException("Signal range [" + signalOffset + ":" + count + "] doesn't fit in the signal");
			}
			if ((arrayOffset < 0) || ((arrayOffset + count) > target.length)) {
				throw new IndexOutOfBoundsException("Target range [" + arrayOffset + ":" + count + "] doesn't fit in the target array");
			}
			if (uniformSampling || channelSampling[channel] == samplingFrequency) {
				getRawSamples(channel, target, signalOffset, count, arrayOffset);
			} else {
				resampler.resample(this, channel, target, signalOffset, count, arrayOffset, samplingFrequency, channelSampling[channel]);
			}

			if (this.calibrationCapable) {
				float gain;
				try {
					gain = this.reader.get_calibration(channel);
				} catch (SignalMLCodecException e) {
					logger.error("get_calibration(channel) failed", e);
					try {
						gain = this.reader.get_calibration();
					} catch (SignalMLCodecException e2) {
						logger.error("get_calibration() failed", e2);
						throw new RuntimeException(e);
					}
				}
				float offset = this.getCalibrationOffset()[channel];
				//logger.debug(String.format("[%d] gain=%f offset=%f", channel, gain, offset));
				for (int i=0; i<count; i++) {
					target[arrayOffset + i] *= gain;
					target[arrayOffset + i] += offset;
				}
			}
		}
	}

	@Override
	public void getRawSamples(int channel, double[] target, int signalOffset, int count, int arrayOffset) {
		synchronized (this) {
			int targetIndex = arrayOffset;
			int i = signalOffset;
			int limit = signalOffset + count;
			try {
				for (; i<limit; i++) {
					target[targetIndex] = reader.getChannelSample(i, channel);
					targetIndex++;
				}
			} catch (SignalMLCodecException ex) {
				logger.error("Failed to get sample, filling the rest with zero and exiting", ex);
				for (; i<limit; i++) {
					target[targetIndex] = 0.0F;
					targetIndex++;
				}
				return;
			}
		}
	}

	/**
	 * Sets the calibration to the given value and tries to update
	 * it in the file.
	 * @param calibration the new value of calibration
	 */
	@Override
	public void setCalibrationGain(float calibration) {
		synchronized (this) {
			if (this.calibration != calibration) {
				float last = this.calibration;
				this.calibration = calibration;
				calibrationExternal = true;
				try {
					reader.set_calibration(calibration);
				} catch (SignalMLCodecException ex) {
					logger.error("Failed to propagate calibration to the codec", ex);
				}
				pcSupport.firePropertyChange(CALIBRATION_PROPERTY, new Float(last), new Float(calibration));
			}
		}
	}

	@Override
	public void setSamplingFrequency(float samplingFrequency) {
		synchronized (this) {
			if (this.samplingFrequency != samplingFrequency) {
				float last = this.samplingFrequency;
				this.samplingFrequency = samplingFrequency;
				samplingFrequencyExternal = true;
				try {
					reader.set_sampling_frequency(samplingFrequency);
				} catch (SignalMLCodecException ex) {
					logger.error("Failed to propagate sampling frequency to the codec", ex);
				}
				if (!uniformSampling) {
					collectChannelSampling();
				}
				pcSupport.firePropertyChange(SAMPLING_FREQUENCY_PROPERTY, new Float(last), new Float(samplingFrequency));
			}
		}
	}

	@Override
	public void setChannelCount(int channelCount) {
		synchronized (this) {
			if (this.channelCount != channelCount) {
				int last = this.channelCount;
				this.channelCount = channelCount;
				channelCountExternal = true;
				try {
					reader.set_number_of_channels(channelCount);
				} catch (SignalMLCodecException ex) {
					logger.error("Failed to propagate channel count to the codec", ex);
				}
				readLabels();
				if (!uniformSampling) {
					collectChannelSampling();
				}
				pcSupport.firePropertyChange(CHANNEL_COUNT_PROPERTY, last, channelCount);
			}
		}
	}

	@Override
	public OriginalMultichannelSampleSource duplicate() throws SignalMLException {

		SignalMLCodecReader newReader = reader.getCodec().createReader();
		newReader.open(reader.getCurrentFilename());

		SignalMLCodecSampleSource duplicate = new SignalMLCodecSampleSource(newReader);
		if (channelCountExternal) {
			duplicate.setChannelCount(channelCount);
		}
		if (samplingFrequencyExternal) {
			duplicate.setSamplingFrequency(samplingFrequency);
		}
		if (calibrationExternal) {
			duplicate.setCalibrationGain(calibration);
		}

		return duplicate;

	}

	/**
	 * Closes the file and removes the <code>reader</code> from this
	 * source.
	 */
	@Override
	public void destroy() {
		reader.close();
		reader = null;
	}

	@Override
	public boolean areIndividualChannelsCalibrationCapable() {
		return false;
	}

	@Override
	public void setCalibrationGain(float[] calibration) {
		throw new UnsupportedOperationException("Setting calibration gain for individual channels is not supported for the SignalMLCodecSampleSource");
	}

	@Override
	public float getSingleCalibrationGain() {
		return calibration;
	}

	@Override
	public float[] getCalibrationOffset() {
		float[] calibrationOffset = new float[getChannelCount()];
		Arrays.fill(calibrationOffset, 0);
		return calibrationOffset;
	}

	/**
	 * Setting calibration offset is not supported for the
	 * SignalMLCodecsSampleSource.
	 * @param calibrationOffset
	 */
	@Override
	public void setCalibrationOffset(float[] calibrationOffset) {
		throw new UnsupportedOperationException("Not supported yet.");
	}

	/**
	 * Setting calibration offset is not supported for the
	 * SignalMLCodecsSampleSource.
	 * @param calibrationOffset
	 */
	@Override
	public void setCalibrationOffset(float calibrationOffset) {
		throw new UnsupportedOperationException("Not supported yet.");
	}

	@Override
	public float getSingleCalibrationOffset() {
		return 0.0F;
	}

	@Override
	public void addSignalChangeListener(PluginSignalChangeListener listener) {
		//this sample source doesn't support signalchangelisteners.
	}

}
